package com.awcall.iot.utils;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import okhttp3.*;
import okhttp3.internal.Util;
import okio.BufferedSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

import javax.net.ssl.*;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class OkHttpUtils {

    private static OkHttpUtils instance = new OkHttpUtils();

    private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");
    private static final MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");

    private static final Logger log = LoggerFactory.getLogger(OkHttpUtils.class);
    private static final String CHAR_SET = "UTF-8";
    private static final String GET = "GET";
    private static final String POST = "POST";
    private static final String PUT = "PUT";
    private static final String DELETE = "DELETE";
    private static final String SSL = "SSL";
    private static final String TLS = "TLS";
    private static final int CONNECT_TIMEOUT = 10;
    private static final int WRITE_TIMEOUT = 10;
    private static final int READ_TIMEOUT = 30;

    private Map<String, String> headers = new HashMap<>();
    private Map<String, String> queryParams = new HashMap<>();
    private Map<String, String> formParams = new HashMap<>();
    private Map<String, List<File>> formFiles = new HashMap<>();
    private String method = GET;
    private String sslStandard = SSL;
    private String url;
    private Response response;
    private RequestBody requestBody;
    private KeyManager[] keyManagers;

    public Map<String, String> asXmlMap() {
        return XmlUtils.xmlToMap(asString());
    }

    public static OkHttpUtils getInstance() {
        return instance;
    }

    public OkHttpUtils() {
    }

    public OkHttpUtils(String url) {
        this.url = url;
    }

    public Response response() {
        if (response == null) {
            throw new RuntimeException("response is null");
        }
        return response;
    }

    public OkHttpUtils url(String url) {
        this.url = url;
        return this;
    }

    public OkHttpUtils header(String name, String value) {
        headers.put(name, value);
        return this;
    }

    public OkHttpUtils headers(Map<String, String> headers) {
        this.headers = headers;
        return this;
    }

    public OkHttpUtils queryParam(String name, String value) {
        queryParams.put(name, value);
        return this;
    }

    public OkHttpUtils queryParams(Map<String, String> queryParams) {
        this.queryParams = queryParams;
        return this;
    }

    public OkHttpUtils formParam(String name, String value) {
        queryParams.put(name, value);
        return this;
    }

    public OkHttpUtils formParams(Map<String, String> formParams) {
        this.formParams = formParams;
        return this;
    }

    public OkHttpUtils formFile(String name, File file) {
        List<File> fileList = formFiles.get(name);
        if (CollectionUtils.isEmpty(fileList)) {
            formFiles.put(name, Stream.of(file).collect(Collectors.toList()));
        } else {
            fileList.add(file);
        }
        return this;
    }

    public OkHttpUtils formFiles(String name, List<File> fileList) {
        formFiles.put(name, fileList);
        return this;
    }

    public OkHttpUtils formFiles(Map<String, List<File>> files) {
        this.formFiles = files;
        return this;
    }

    public OkHttpUtils get() {
        method = GET;
        return this;
    }

    public OkHttpUtils post() {
        method = POST;
        requestBody = Util.EMPTY_REQUEST;
        return this;
    }

    public OkHttpUtils post(String body) {
        method = POST;
        requestBody = RequestBody.create(MEDIA_TYPE_TEXT, body);
        return this;
    }

    public OkHttpUtils post(Object body) {
        method = POST;
        requestBody = RequestBody.create(MEDIA_TYPE_JSON, JsonUtils.toJson(body));
        log.debug(JsonUtils.toJson(body));
        return this;
    }

    public OkHttpUtils post(String body, MediaType mediaType) {
        method = POST;
        requestBody = RequestBody.create(mediaType, body);
        return this;
    }

    public OkHttpUtils put() {
        method = PUT;
        requestBody = Util.EMPTY_REQUEST;
        return this;
    }

    public OkHttpUtils put(String body) {
        method = PUT;
        requestBody = RequestBody.create(MEDIA_TYPE_TEXT, body);
        return this;
    }

    public OkHttpUtils put(Object body) {
        method = POST;
        requestBody = RequestBody.create(MEDIA_TYPE_JSON, JsonUtils.toJson(body));
        return this;
    }

    public OkHttpUtils put(String body, MediaType mediaType) {
        method = PUT;
        requestBody = RequestBody.create(mediaType, body);
        return this;
    }

    public OkHttpUtils delete() {
        method = DELETE;
        return this;
    }

    public OkHttpUtils delete(String body) {
        method = DELETE;
        requestBody = RequestBody.create(MEDIA_TYPE_TEXT, body);
        return this;
    }

    public OkHttpUtils delete(Object body) {
        method = POST;
        requestBody = RequestBody.create(MEDIA_TYPE_JSON, JsonUtils.toJson(body));
        return this;
    }

    public OkHttpUtils delete(String body, MediaType mediaType) {
        method = DELETE;
        requestBody = RequestBody.create(mediaType, body);
        return this;
    }

    public OkHttpUtils ssl(InputStream pfxCertificate, char[] pfxPassword) {
        if (pfxCertificate == null || pfxPassword == null) {
            return null;
        }
        try {
            KeyStore clientKS = KeyStore.getInstance("PKCS12");
            clientKS.load(pfxCertificate, pfxPassword);
            KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            factory.init(clientKS, pfxPassword);
            keyManagers = factory.getKeyManagers();
            sslStandard = TLS;
            return this;
        } catch (IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyStoreException e) {
            throw new RuntimeException(e);
        }
    }

    public RequestBody requestBody() {
        if (!CollectionUtils.isEmpty(formFiles)) {
            MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
            formFiles.forEach((name, fileList) ->
                    fileList.forEach(file ->
                            builder.addFormDataPart(name, file.getName(), RequestBody.create(MultipartBody.FORM, file))
                    )
            );
            formParams.forEach(builder::addFormDataPart);
            return builder.build();
        }

        if (!CollectionUtils.isEmpty(formParams)) {
            log.debug(buildQueryString(formParams));
            FormBody.Builder formBuilder = new FormBody.Builder();
            formParams.forEach((key, value) -> {
                if (value != null) {
                    formBuilder.add(key, value);
                }
            });
            return formBuilder.build();
        }

        return requestBody;
    }

    public OkHttpUtils execute() {
        Request request = new Request.Builder()
                .url(buildUrlWithQueryString(url, queryParams))
                .method(method, requestBody())
                .headers(Headers.of(headers))
                .build();
        try {
            response = okHttpClient().newCall(request).execute();
            log.debug(response.toString());
            return this;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public int code() {
        return response().code();
    }

    public String message() {
        return response().message();
    }

    public ResponseBody body() {
        return response().body();
    }

    public String asString() {
        try {
            String result = body().string();
            log.debug(result);
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            response().close();
        }
    }

    public String asString(String charset) {
        BufferedSource source = body().source();
        try {
            String result = source.readString(Charset.forName(charset));
            log.debug(result);
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                source.close();
                response().close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public File asFile(String path) {
        return download(new File(path));
    }

    public File asFile(File file) {
        return download(file);
    }

    public JsonNode asJsonNode() {
        return JsonUtils.parse(asString());
    }

    public <T> T asObject(Class<T> clazz) {
        return JsonUtils.parse(asString(), clazz);
    }

    public <T> T asObject(TypeReference<T> type) {
        return JsonUtils.parse(asString(), type);
    }

    private File download(File file) {
        try {
            InputStream inputStream = body().byteStream();
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            FileCopyUtils.copy(inputStream, fileOutputStream);
            return file;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static String buildUrlWithQueryString(String url, Map<String, String> queryParas) {
        if (CollectionUtils.isEmpty(queryParas)) {
            return url;
        }

        StringBuilder sb = new StringBuilder(url);
        if (!url.contains("?")) {
            sb.append("?");
        }
        return sb.append(buildQueryString(queryParas)).toString();
    }

    public static String buildQueryString(Map<String, String> queryParas) {
        StringBuilder sb = new StringBuilder();
        final boolean[] isFirst = {true};
        queryParas.forEach((key, value) -> {
            if (value != null) {
                if (isFirst[0]) {
                    isFirst[0] = false;
                } else {
                    sb.append("&");
                }
                if (!StringUtils.isEmpty(value)) {
                    try {
                        value = URLEncoder.encode(value, CHAR_SET);
                    } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException(e);
                    }
                }
                sb.append(key).append("=").append(value);
            }
        });
        return sb.toString();
    }

    private static X509TrustManager trustManager() {
        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    }

    private OkHttpClient okHttpClient() {
        try {
            X509TrustManager trustManager = trustManager();
            SSLContext sslContext = SSLContext.getInstance(sslStandard);
            sslContext.init(keyManagers, new TrustManager[]{trustManager}, new SecureRandom());
            return new OkHttpClient().newBuilder()
                    .sslSocketFactory(sslContext.getSocketFactory(), trustManager)
                    .hostnameVerifier((hostname, session) -> true)
                    .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
                    .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
                    .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
                    .build();
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

}
